package util;


import org.apache.commons.lang3.tuple.Pair;
import org.bytedeco.javacpp.FloatPointer;
import org.bytedeco.javacpp.indexer.FloatRawIndexer;
import org.bytedeco.opencv.global.opencv_core;
import org.bytedeco.opencv.global.opencv_imgcodecs;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.global.opencv_photo;
import org.bytedeco.opencv.opencv_core.*;
import org.bytedeco.opencv.opencv_imgproc.Subdiv2D;
import org.bytedeco.opencv.opencv_imgproc.Vec6fVector;

import java.io.File;
import java.util.*;

import static org.bytedeco.opencv.global.opencv_core.*;
import static org.bytedeco.opencv.global.opencv_imgproc.CV_AA;
import static org.bytedeco.opencv.global.opencv_imgproc.INTER_LINEAR;
import static org.bytedeco.opencv.global.opencv_photo.fastNlMeansDenoisingColored;


/**
 * @ClassName: OpenCVFaceSwap
 * @description: 人脸融合方法
 * @author: ShiQiang
 * @create: 2020年5月29日14:03:10
 **/
public class OpenCVFaceSwap {

    public static String savePath ;

    /**
     * @author ShiQiang
     * @date 2020年5月29日14:03:25
     * @param imgPath1
     * @param imgPath2
     * @param path
     * @param type
     * @param jingxi
     */
    public static void faceMerge(String imgPath1,String imgPath2,String path,String type,boolean jingxi){
        savePath = path;
        // 两张图片地址
        String path1 = imgPath1;
        String path2 = imgPath2;
        // load the two images.
        Mat imgCV1 = opencv_imgcodecs.imread(path1);
        Mat imgCV2 = opencv_imgcodecs.imread(path2);
        if (null == imgCV1 || imgCV1.cols() <= 0 || null == imgCV2 || imgCV2.cols() <= 0) {
            System.out.println("There is wrong with images");
            return;
        }
        //ImageUI ui1 = new ImageUI();
        //ImageUI ui2 = new ImageUI();
        //ui1.imshow("原始图像A", imgCV1);
        //ui2.imshow("原始图像B", imgCV2);
        List<Point2f> points12f = new ArrayList<>();
        List<Point2f> points22f = new ArrayList<>();
        // 人脸识别出的关键点
        if ("baidu".equals(type)) {
            points12f = FaceDetect.detect(path1);
            points22f = FaceDetect.detect(path2);
        }
        if ("opencv".equals(type)) {
            points12f = FaceDetect.detect(imgCV1.clone());
            points22f = FaceDetect.detect(imgCV2.clone());
            //---去掉多余的点位
            points12f.remove(64);
            points12f.remove(63);
            points12f.remove(62);
            points12f.remove(61);
            points12f.remove(60);
            points22f.remove(64);
            points22f.remove(63);
            points22f.remove(62);
            points22f.remove(61);
            points22f.remove(60);
        }
        if (!jingxi) {
            //通过凸包进行融合
            Pair<List<Point2f>,List<Point2f>> pair = getConvexHull(imgCV1.clone(),imgCV2.clone(),points12f,points22f);
            points12f = pair.getLeft();
            points22f = pair.getRight();
        }

        drawPoints(imgCV1.clone(),points12f,"1");
        drawPoints(imgCV2.clone(),points22f,"2");
        //----------------------三角剖分------------------------------
        Pair<Mat,Mat> wa  = toWarpAffine(imgCV1,imgCV2,points12f,points22f);
        Mat _srcImg2 = wa.getLeft();
        Mat srcImg1 = wa.getRight();
        //---------------------获取图2的面部轮廓-------------------------------------
        Pair<List<Point>,List<Point2f>>  hulls = getConvexHull(points22f);
        List<Point> convexPoints2 = hulls.getLeft();
        List<Point2f> convexPoints2f = hulls.getRight();
        //---------------------图像融合-------------------------------------
        //因为经过仿射变换之后的人脸只是形状上吻合，但是边缘太生硬，需要进行图像融合
        //凸包所在的点，组成的集合其实是人脸边界轮廓
        //制作mask
        MatExpr face = Mat.zeros(imgCV2.size(), CV_8UC1);
        Mat faceMask = face.asMat();
        opencv_imgproc.fillConvexPoly(faceMask,list2MP(convexPoints2), new Scalar(255, 255));
        //采用seamlessClone进行图像融合，效果较好，Microsoft NB的算法,无缝融合功能
        //获取最小矩形
        Rect r = opencv_imgproc.boundingRect(list2MP2(convexPoints2f));
        System.out.println(r.x()+"=="+r.y()+"=="+r.width()+"=="+r.height());
        Point center = new Point((int)(r.tl().x() + r.br().x())/2, (int)(r.tl().y() + r.br().y())/2);
        Mat resultImg = new Mat();
        opencv_photo.seamlessClone(_srcImg2, imgCV2, faceMask, center, resultImg,opencv_photo.NORMAL_CLONE);

        //---------------------过程图片展示-------------------------------------
        Mat[] triangleImg = new Mat[2];
        triangleImg[0] = srcImg1.clone();
        triangleImg[1] = imgCV2.clone();

        drawTriangles("3",triangleImg[1], triangles2, new Scalar(255, 255));
        drawTriangles("4",triangleImg[0], triangles1, new Scalar(255, 255));

        opencv_imgcodecs.imwrite(savePath+ File.separator  + "srcImg1.jpg", srcImg1);
        opencv_imgcodecs.imwrite(savePath+ File.separator  + "srcImg2.jpg", imgCV2);

        opencv_imgcodecs.imwrite(savePath+ File.separator  + "temp.jpg", _srcImg2);
        opencv_imgcodecs.imwrite(savePath+ File.separator  + "result.jpg", resultImg);

    }

    /**
     * 三角图片仿射变幻
     * 思路
     * 1、对图1进行三角剖分
     * 2、根据图像1的索引结果，得到图像2的三角剖分
     * 3、计算图像1的每个三角形到图像2对应的三角形的仿射变换矩阵
     * 4、将图1的每个三角copy到图2上面进行覆盖
     * 5、返回覆盖后的图片数据
     * @param imgCV1
     * @param imgCV2
     * @param points12f
     * @param points22f
     * @return
     */
    public static Pair<Mat,Mat> toWarpAffine(Mat imgCV1,Mat imgCV2,List<Point2f> points12f,List<Point2f> points22f){
        //----------------------三角剖分------------------------------

        //1、只需要对图1进行三角剖分即可
        // delaunay triangulation 三角剖分和仿射变换
        Rect rect = new Rect(0, 0, imgCV1.cols(), imgCV1.rows());
        List<Correspondens> delaunayTri = delaunayTriangulation(imgCV1.clone(),points12f, rect);
        //--------------------------仿射变换-----------------------------------
        //根据图像1的索引结果，得到图像2的三角剖分
        for(int i=0;i<delaunayTri.size();++i) {
            Correspondens corpd = delaunayTri.get(i);
            List<Point> tring = new ArrayList<>();
            tring.add(new Point((int)points22f.get(corpd.getIndex().get(0)).x(),(int)points22f.get(corpd.getIndex().get(0)).y()));
            tring.add(new Point((int)points22f.get(corpd.getIndex().get(1)).x(),(int)points22f.get(corpd.getIndex().get(1)).y()));
            tring.add(new Point((int)points22f.get(corpd.getIndex().get(2)).x(),(int)points22f.get(corpd.getIndex().get(2)).y()));
            triangles2.add(tring);
        }

        Mat _srcImg2 = imgCV2.clone();  //图像2进行复制，目的是保留原始图像
        Mat srcImg1 = imgCV1.clone();  //图像1进行复制，目的是保留原始图像
        //drawTriangles("2",_srcImg2, triangles2, new Scalar(255, 255));
        //drawTriangles("1",srcImg1, triangles1, new Scalar(255, 255));
        //仿射变换
        for (int i = 0; i < triangles1.size(); i++){
            //确定ROI 计算轮廓的垂直边界最小矩形
            Rect roi_1 = opencv_imgproc.boundingRect(list2MP(triangles1.get(i)));
            Rect roi_2 = opencv_imgproc.boundingRect(list2MP(triangles2.get(i)));
            //ROI区域的图像,图像1
            Mat roi_img = new Mat(srcImg1,roi_1);
            //减去ROI左上角坐标得到 每个三角的边长
            List<Point2f> triangle1_nor = new ArrayList<>();
            List<Point2f> triangle2_nor = new ArrayList<>();
            for (Point j : triangles1.get(i)){
                float x = j.x() - roi_1.tl().x();
                float y = j.y() - roi_1.tl().y();
                triangle1_nor.add(new Point2f(x, y));
            }
            for (Point k : triangles2.get(i)){
                float x = k.x() - roi_2.tl().x()+1;  //此处加1解决拼接的图片有缝隙问题
                float y = k.y() - roi_2.tl().y()+1;
                triangle2_nor.add(new Point2f(x, y));
            }
            //计算图像1的每个三角形到图像2对应的三角形的仿射变换矩阵
            Mat M = opencv_imgproc.getAffineTransform(list2MP2(triangle1_nor), list2MP2(triangle2_nor));
            //仿射变换
            Mat imgWarp = new Mat();
            //opencv_imgproc.warpAffine(roi_img, imgWarp, M, roi_2.size()); //, 1, opencv_core.BORDER_REFLECT_101
            //int flags=INTER_LINEAR, int borderMode=BORDER_CONSTANT, const Scalar& borderValue=Scalar()
            opencv_imgproc.warpAffine(roi_img, imgWarp, M, roi_2.size(), INTER_LINEAR, BORDER_REFLECT_101,new Scalar(0,0) ); //, 1, opencv_core.BORDER_REFLECT_101
            //制作图像2的局部mask
            MatExpr maskExpr = Mat.zeros(roi_2.size(), CV_8U);
            Mat mask = maskExpr.asMat();
            //fillConvexPoly绘图函数输入坐标必须为int类型
            List<Point> triangle2_nor_int = new ArrayList<>();
            for (int pn=0;pn<triangle2_nor.size();pn++){
                Point2f pf = triangle2_nor.get(pn);
                triangle2_nor_int.add(new Point((int)pf.x(),(int)pf.y()));
            }
            //填充凸多边形
            opencv_imgproc.fillConvexPoly(mask,list2MP(triangle2_nor_int), new Scalar(255,255));
            Mat imageROI = new Mat(_srcImg2, roi_2);
            imgWarp.copyTo(imageROI,mask);
        }
        return Pair.of(_srcImg2,srcImg1);
    }


    //给图像画点
    public static void drawPoints( Mat face1,List<Point2f> points1,String name){
        for(int i=0;i<points1.size();i++){
            opencv_imgproc.putText(face1,i+"", new Point((int)points1.get(i).x(),(int)points1.get(i).y()), opencv_imgproc.FONT_HERSHEY_SCRIPT_SIMPLEX,0.3, new Scalar(255, 0, 0, 0));
        }
        opencv_imgcodecs.imwrite(savePath+ File.separator + name +"FACE.jpg", face1);
    }
    //根据点给图像画线-三角形
    public static void drawTriangles(String name,Mat img,List<List<Point>> triangles, Scalar color){

        for (List<Point> points : triangles){
           /* MatVector p = new MatVector();
            for (Point j:i){
                p.push_back(new Mat(j));
            }
            opencv_imgproc.polylines(img, p, false, color);
            p.clear();*/

            opencv_imgproc.line(img, points.get(0), points.get(1), new Scalar(255, 255) , 1, CV_AA, 0);
            opencv_imgproc.line(img, points.get(1), points.get(2), new Scalar(255, 255) , 1, CV_AA, 0);
            opencv_imgproc.line(img, points.get(2), points.get(0), new Scalar(255, 255) , 1, CV_AA, 0);
        }
        opencv_imgcodecs.imwrite(savePath+ File.separator  +name +"SANJIAO1.jpg", img);
    }
    /**
     * 获取Delaunay三角形的列表
     * @param hull
     * @param rect
     * @return
     */
    static List<List<Point>> triangles1 = new ArrayList<>();
    static List<List<Point>> triangles2 = new ArrayList<>();

    public static List<Correspondens> delaunayTriangulation(Mat img ,List<Point2f> hull, Rect rect) {
        Subdiv2D subdiv = new Subdiv2D(rect);

        for(int it = 0; it < hull.size(); it++) {
            subdiv.insert(hull.get(it));
        }
        Vec6fVector triangles = new Vec6fVector();
        subdiv.getTriangleList(triangles);
        // subdiv.getVoronoiFacetList(triangles);
        FloatPointer[] floatPs = triangles.get();
        long cnt = floatPs.length;
        List<Correspondens> delaunayTri = new LinkedList<Correspondens>();
        for(int i = 0; i < triangles.size(); ++i) {
            List<Point> points = new LinkedList<Point>();
            FloatPointer t = triangles.get(i);
            //获取三角行的顶点
            points.add(new Point((int) t.get(0), (int) t.get(1)));
            points.add(new Point((int)t.get(2), (int)t.get(3)));
            points.add(new Point((int)t.get(4), (int)t.get(5)));
            Correspondens ind = new Correspondens();
            //判断顶点是否在ROI矩形内
            if (rect.contains(points.get(0)) && rect.contains(points.get(1)) && rect.contains(points.get(2))) {

                int count = 0;
                for (int j = 0; j < 3; j++) {
                    for (int k = 0; k < hull.size(); k++) {
                        if (Math.abs(points.get(j).x() - hull.get(k).x()) < 1.0 && Math.abs(points.get(j).y() - hull.get(k).y()) < 1.0) {
                            ind.add(k);
                            count++;
                        }
                    }
                }
                if (count == 3){
                    triangles1.add(points);
                    delaunayTri.add(ind);
                }

            }
        }
        return delaunayTri;
    }

    /**
     *
     * @param img1
     * @param img2
     * @param t1
     * @param t2
     * @param z
     * @return
     */
    public static Mat warpTriangle(Mat img1, Mat img2, List<Point> t1, List<Point> t2, int z) {

        Mat points1 = new Mat(t1.size());
        for(Point pf : t1){
            Mat pm = new Mat(pf);
            points1.push_back(pm);
        }
        Mat points2 = new Mat(t2.size());
        for(Point pf : t2){
            Mat pm = new Mat(pf);
            points2.push_back(pm);
        }

        Rect r1 = opencv_imgproc.boundingRect(points1);
        Rect r2 = opencv_imgproc.boundingRect(points2);

        Point[] t1Points = new Point[t1.size()];
        t1.toArray(t1Points);

        Point[] t2Points =new Point[t2.size()];
        t2.toArray(t2Points);

        List<Point2f> t1Rect = new LinkedList<Point2f>();
        List<Point2f> t2Rect = new LinkedList<Point2f>();
        List<Point> t2RectInt = new LinkedList<Point>();

        for (int i = 0; i < 3; i++) {
            t1Rect.add(new Point2f(t1Points[i].x() - r1.x(), t1Points[i].y() - r1.y()));
            t2Rect.add(new Point2f(t2Points[i].x() - r2.x(), t2Points[i].y() - r2.y()));
            t2RectInt.add(new Point(t2Points[i].x() - r2.x(), t2Points[i].y() - r2.y()));
        }
        // mask 包含目标图片三个凸点的黑色矩形
       // MatExpr maskExpr = Mat.zeros(r2.height(), r2.width(), opencv_core.CV_32FC3);
        Mat mask = new Mat(r2.height(), r2.width(), CV_32FC3,new Scalar(0,0));
        opencv_imgproc.fillConvexPoly(mask,list2MP(t2RectInt), new Scalar(1.0, 1.0), 16, 0);

        Mat img1Rect = new Mat();
        FloatRawIndexer imgIndex = img1.createIndexer();
        //img1.submat(r1).copyTo(img1Rect);
        new Mat(img1,r1).copyTo(img1Rect);

        //img1.apply(r1).copyTo(img1Rect);
        // img2Rect 原始图片适应mask大小并调整位置的图片
        System.out.println(r2.height() + "======" + r2.width() + "=====" + img1Rect.type());
        MatExpr img2RectExpr = Mat.zeros(r2.height(), r2.width(), img1Rect.type());
        Mat img2Rect = img2RectExpr.asMat();
        img2Rect = applyAffineTransform(img2Rect, img1Rect, t1Rect, t2Rect);
        System.out.println(img2Rect.rows() + "======" + img2Rect.cols() + "=====" + img2Rect.type());
        opencv_core.multiply(img2Rect, mask, img2Rect); // img2Rect在mask三个点之间的图片
        Mat dst = new Mat();
        System.out.println(img2Rect.rows() + "======" + img2Rect.cols() + "=====" + img2Rect.type());

        MatExpr img2RectS = Mat.ones(r2.height(), r2.width(), CV_32FC3);
        Mat img2s = img2RectS.asMat();
        opencv_core.subtract(mask, img2s, dst);
        opencv_core.multiply(img2.apply(r2), dst, img2.apply(r2));
        opencv_core.absdiff(img2.apply(r2), img2Rect, img2.apply(r2));
        mask.release();
        return img2;
    }

    public static Mat applyAffineTransform(Mat warpImage, Mat src,List<Point2f> srcTri, List<Point2f> dstTri) {
        Mat warpMat = opencv_imgproc.getAffineTransform(list2MP2(srcTri),list2MP2(dstTri));
        opencv_imgproc.warpAffine(src, warpImage, warpMat, warpImage.size()); //, opencv_imgproc.INTER_LINEAR
        return warpImage;
    }

    /**
     * List exchange to MatOfPoint
     * @param points
     * @return
     */
    public static Mat list2MP(List<Point> points) {
        Mat points2 = new Mat(points.size());
        for(Point pf : points){
            Mat pm = new Mat(pf);
            points2.push_back(pm);
        }
        return points2;
    }

    public static Mat list2MP2d(List<Point2d> points) {
        Mat points2 = new Mat(points.size());
        for(Point2d pf : points){
            Mat pm = new Mat(pf);
            points2.push_back(pm);
        }
        return points2;
    }
    /**
     * List exchange to MatOfPoint2f
     * @param points
     * @return
     */
    public static Mat list2MP2(List<Point2f> points) {
        Mat points2 = new Mat(points.size());
        for(Point2f pf : points){
            Mat pm = new Mat(pf);
            points2.push_back(pm);
        }
        return points2;
    }
    //计算凸包办法
    public static Pair<List<Point>,List<Point2f>> getConvexHull(List<Point2f> points){
        // 计算凸包
        Mat convexPointsIdx2 = new Mat();
        //用了好久的时间实验出来的
        Mat points2m = new Mat(points.size());
        for(Point2f pf : points){
            Mat pm = new Mat(pf);
            points2m.push_back(pm);
        }
        //寻找凸包
        opencv_imgproc.convexHull(points2m, convexPointsIdx2,false,true);
        FloatRawIndexer matIndex = convexPointsIdx2.createIndexer();
        List<Point> convexPoints2 = new LinkedList<Point>();
        List<Point2f> convexPoints2f = new LinkedList<Point2f>();
        long rows = convexPointsIdx2.rows();
        for (int i = 0; i < rows; i++) {
            float x = matIndex.get(i,0);
            float y = matIndex.get(i,1);
            convexPoints2.add(new Point((int)x,(int)y));
            convexPoints2f.add(new Point2f(x,y));
        }
        return Pair.of(convexPoints2,convexPoints2f);

    }
    //计算凸包办法
    public static Pair<List<Point2f>,List<Point2f>> getConvexHull(Mat imgCV2,Mat imgCV1,List<Point2f> points1,List<Point2f> points2){
        // 计算凸包
        Mat imgCV1Warped = imgCV2.clone();
        imgCV1.convertTo(imgCV1, CV_32FC3);
        imgCV1Warped.convertTo(imgCV1Warped, CV_32FC3);

        Mat hull = new Mat();
        //用了好久的时间实验出来的
        Mat points2m = new Mat(points2.size());
        for(Point2f pf : points2){
            Mat pm = new Mat(pf);
            points2m.push_back(pm);
        }
        opencv_imgproc.convexHull(points2m, hull,false,true);


        FloatRawIndexer matIndex = hull.createIndexer();
        List<Point2f> hull1 = new LinkedList<Point2f>();
        List<Point2f> hull2 = new LinkedList<Point2f>();
        // 保存组成凸包的关键点
        List<Point2f> hullPoinst = new LinkedList<Point2f>();
        FloatRawIndexer hullIndex = hull.createIndexer();
        long rows = hull.rows(), cols = hull.cols();
        for (int i = 0; i < rows; i++) {
            hullPoinst.add(new Point2f(hullIndex.get(i,0),hullIndex.get(i,1)));
        }
        if (hullPoinst.size() > 0) {
            for(Point2f hp : hullPoinst){
                for(int j=0,totalj=points1.size();j<totalj;j++){
                    if (hp.x() == points2.get(j).x() && hp.y() == points2.get(j).y()) {
                        hull1.add(points1.get(j));
                        hull2.add(points2.get(j));
                    }
                }
            }
        }
        //画凸包
        /*Mat face3 = imgCV1.clone();
        for(int i=0;i<hull1.size();i++){
            opencv_imgproc.putText(face3,i+"", new Point((int)hull1.get(i).x(),(int)hull1.get(i).y()), opencv_imgproc.CV_FONT_HERSHEY_SIMPLEX,0.3, new Scalar(255, 0, 0, 0));
        }
        opencv_imgcodecs.imwrite("F:/face/"+ File.separator + "FACE3.jpg", face3);
        Mat face4 = imgCV2.clone();
        for(int i=0;i<hull2.size();i++){
            opencv_imgproc.putText(face4,i+"", new Point((int)hull2.get(i).x(),(int)hull2.get(i).y()), opencv_imgproc.CV_FONT_HERSHEY_SIMPLEX,0.3, new Scalar(255, 0, 0, 0));
        }
        opencv_imgcodecs.imwrite("F:/face/"+ File.separator + "FACE4.jpg", face4);*/
        //===画凸包
        return  Pair.of(hull1, hull2);
    }
}
